package com.wang.tool.common;

import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 热点探针
 * 此工具和一般信号量的锁有什么区别?
 * isHot 函数仅仅是判断是否超过目标阈值,没有用完归还的逻辑,全过程不阻塞
 *
 * @Author wangshaopeng
 * @Date 2022/6/19
 */
public class HotProbe {

    /**
     * 访问暂存,版本号(乐观锁)-更新时间戳-剩余的令牌
     * 1w个存储,假设1s的服务器满载qps是1000,map最大值1w足够10s,理论上足够用
     */
    private final Map<String, Triplet<AtomicLong, AtomicLong, AtomicInteger>> visitSubjects = new LruMap<>(10000);

    /**
     * 每秒访问阈值,超过这个值就算做热点
     */
    private int hotThreshold;


    /**
     * 初始化一个探针
     *
     * @param hotThreshold
     * @return
     */
    public static HotProbe newInstance(int hotThreshold) {
        HotProbe hotProbe = new HotProbe();
        hotProbe.setHotThreshold(hotThreshold);
        return hotProbe;
    }

    /**
     * 访问一次并判断是不是单机热点
     * 统计方式:1s的滑动窗口
     *
     * @param key key
     * @return 是否是热点
     */
    public boolean isHot(String key) {
        //参数或配置不合理统一当做非
        if (key==null || hotThreshold < 1) {
            return false;
        }

        //循环解决资源争抢问题
        while (true) {
            long now = System.currentTimeMillis();
            Triplet<AtomicLong, AtomicLong, AtomicInteger> visitSubject = visitSubjects.putIfAbsent(key, new Triplet<>(new AtomicLong(0), new AtomicLong(now), new AtomicInteger(hotThreshold - 1)));
            if (visitSubject == null) {
                //无人访问,返回false,令牌数量-1
                return false;
            }

            AtomicLong versionAtomic = visitSubject.getLeft();
            long version = versionAtomic.get();
            //计算时间窗口宽度,当前时间跟上一次更新时间经过的时间
            AtomicLong updateTimeAtomic = visitSubject.getMiddle();
            long pastTime = now - updateTimeAtomic.get();

            //拿到之前剩余的令牌数
            AtomicInteger remainTokenAtomic = visitSubject.getRight();
            int remainToken = remainTokenAtomic.get();

            if (pastTime > 1000) {
                //窗口期外
                //拿到应得的令牌桶的数量:时间窗口经过的时间（秒级）* 一秒中的最大请求量
                int dueToken = (int) ((pastTime * hotThreshold) / 1000);
                //计算当前的令牌数,计算规则如下:(应得的令牌数+剩余的令牌数)和阈值取小的那一个!
                //阈值是2,经过了3秒,dueToken为3*2/1时,虽然算出来的是6,但是也只能取2
                int calToken = Math.min(dueToken + remainToken, hotThreshold);
                //如果小于1,证明当前访问的次数已经>=时间窗口容量阈值
                if (calToken < 1) {
                    return true;
                }
                //乐观锁替换版本
                if (versionAtomic.compareAndSet(version, version + 1)) {
                    //设置更新时间
                    updateTimeAtomic.set(now);
                    //设置令牌数
                    remainTokenAtomic.set(calToken - 1);
                    return false;
                }
                //命中乐观锁,重试
                Thread.yield();
            }
            //窗口期内
            //如果小于1了,证明已经超过阈值了,判断为是
            if (remainToken < 1) {
                return true;
            }
            //乐观锁替换版本
            if (versionAtomic.compareAndSet(version, version + 1)) {
                //设置令牌数
                remainTokenAtomic.set(remainToken - 1);
                return false;
            }
            //命中乐观锁,重试
            Thread.yield();
        }
    }

    /**
     * 清理访问记录
     */
    public void clear() {
        visitSubjects.clear();
    }

    public int getHotThreshold() {
        return hotThreshold;
    }

    public void setHotThreshold(int hotThreshold) {
        this.hotThreshold = hotThreshold;
    }

    @Override
    public String toString() {
        return "HotProbe{" +
                "visitSubjectSize=" + visitSubjects.size() +
                ", threshold=" + hotThreshold +
                '}';
    }
}
